Cache compilation in CI with ccache - #179
Conversation
CI rebuilds every object from scratch on each of the four builds a run performs, across two container images. Wrapping the compilers in ccache lets the compiles hit a content-addressed store instead. Only ccache's store is cached. The build/ tree and .sconsign.dblite stay out of it: those are incremental state whose correctness depends on scons's dependency scanner having recorded every input, and it does not record all of them -- DET_INIT is read straight from the environment, and the Configure results in config.h and options_cache.py are not tracked either. Persisting that across runs is how a stale object survives a source change. ccache re-hashes the source, every header it includes by content, the compiler binary and the full command line on every compile, so scons still rebuilds its dependency graph from a fresh checkout each run and only the individual compiles are reused. CCACHE_SLOPPINESS is left unset for the same reason: include_file_mtime and include_file_ctime would trade content hashing for timestamps, and time_macros would let ccache serve a stale __DATE__/__TIME__ banner for GlobalContainerArgs.cpp. CCACHE_COMPILERCHECK=content covers a g++ point release landing inside a base image. Two tripwires: the per-run statistics are printed, so a change to a widely included header that still shows near-total direct hits is visible, and the existing harnesses already run against the produced binaries. Only master writes the cache, and it writes whenever the compiles ran, including runs a later test step failed -- the entries are content-addressed, so a failing test says nothing about whether the objects are the right objects. Enabled by CCACHE=1 in the environment rather than a scons option, so it cannot stick in options_cache.py. Windows is not covered.
97960c2 to
3d0d242
Compare
|
Review of the ccache change. Six findings, most notable first. None is a correctness blocker for the common path (push-triggered fresh run); 1–3 are edge cases around re-runs and manual dispatch that would silently defeat caching rather than fail loudly.
(written by Junior, my agent) |
|
Bradley approved this change. Updated to current master and addressed the six workflow/wrapper findings in
Added a real SCons/ccache integration test in Linux CI. It passes locally with ccache 4.14: cold build, warm cache hit, changed-header invalidation, unwrapped linking, missing-tool error and duplicate-wrapper guards. This validates cache operation on a small compiled fixture; it does not claim a measured whole-project CI speedup yet. Final Linux/Windows checks are running before merge. |
CI rebuilds every object from scratch on each of the four builds a run performs, across two container images. This wraps the compilers in ccache so the compiles hit a content-addressed store instead.
Only ccache's store is cached
build/and.sconsign.dblitestay out of it. Those are incremental state whose correctness depends on scons's dependency scanner having recorded every input, and it doesn't record all of them —DET_INITis read straight from the environment (SConstruct:322), and the Configure results inconfig.h/options_cache.pyaren't tracked either. Persisting that across runs is how a stale object survives a source change.ccache re-hashes the source, every header it includes by content, the compiler binary and the full command line on every compile. So scons still rebuilds its dependency graph from a fresh checkout every run and decides what to compile from scratch; only the individual
g++ -cinvocations are reused. It's stricter than scons here —DET_INITlands inCXXFLAGS, so ccache keys on it where scons doesn't.CCACHE_SLOPPINESSis deliberately unset:include_file_mtime/include_file_ctimetrade content hashing for timestamps, andtime_macroswould let ccache serve a stale__DATE__/__TIME__banner forGlobalContainerArgs.cpp:347.CCACHE_COMPILERCHECK=contentcovers a g++ point release landing inside a base image.Tripwires
ccache -s -vis printed per run — a change to a widely included header that still shows near-total direct hits means the cache isn't seeing it.A cold build is
gh cache deleteon the twoccache-ubuntu:*entries, then re-run. On demand rather than on a timer: ccache keys on content, so a stale entry needs a hash collision, not a stale clock, and a weekly cold build spends 14 minutes of two runners to re-assert that.Cache lifecycle
Only master saves; PR branches can't read each other's caches anyway, and letting every PR write would churn the quota and evict what PRs restore from. Master saves whenever the compiles ran, including runs a later test step failed — the entries are content-addressed, so a failing harness says nothing about whether the objects are the right objects, and a build that compiled the whole tree and then failed the LAN test shouldn't throw the tree away.
!cancelled()rather thanalways()so a cancelled run doesn't upload during the grace period.CCACHE_MAXSIZE=1G. A single cold build is ~130M per image; each master push touching a widely included header adds roughly another tree's worth before LRU catches up. Entries are keyed byrun_id, so GitHub's 10G repo quota evicts the old ones and only the newest prefix match matters.Enabling
CCACHE=1in the environment, not a scons option, so it can't stick inoptions_cache.py. Shared helper inscons/ccache.py, used by both SConstructs. It resolves ccache viashutil.which— scons scrubsPATHfor build commands, so the usual/usr/lib/ccachesymlink trick would silently do nothing here. Missing ccache withCCACHE=1set is a hard error rather than a silent uncached build.Verified locally
With a logging pass-through shim (no ccache on the dev machine):
CCACHE=1, no ccache onPATH→ aborts withCCACHE is set but ccache was not found on PATH.test/SConstructemitsccache g++ -o TestsRunner.o -c …and the object builds.Configureis fine with the prefixed compiler.CCACHEunset → zeroccacheentries incompile_commands.json, andconfig.h/options_cache.pybyte-identical. Default builds untouched.The first master run will be a cold build that populates the cache; the speedup starts after that.
Not covered
Windows. The msys2 job needs path translation between the MSYS-style
CCACHE_DIRand the Windows pathactions/cachetars, which I couldn't test locally. The SConstruct side already works for it if we want it as a follow-up.